// --- Copyright (c) 2006-2008 Stefan Kyntchev ---
// This software is written and copyrighted by Stefan Kyntchev 
// and BeyondPod Team members. All rights are reserved.
// Author contact: support@beyondpod.mobi
// ------------------------------------------------------------------------
// This file is part of BeyondPod RSS Feed Reader and Podcast manager
// (www.codeplex.com/BeyondPod) 
// BeyondPod is free software: you can redistribute it and/or modify 
// it under the terms of the GNU General Public License as published by 
// the Free Software Foundation, either version 3 of the License, or 
// (at your option) any later version. 
//  
// BeyondPod is distributed in the hope that it will be useful, 
// but WITHOUT ANY WARRANTY; without even the implied warranty of 
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
// GNU General Public License for more details. 
//  
// You should have received a copy of the GNU General Public License
// along with BeyondPod. If not, see <http://www.gnu.org/licenses/>
// ------------------------------------------------------------------------
// COPYRIGHT NOTICE: This copyright notice may NOT be removed, obscured or modified 
// without written consent from the author.
// --- End copyright notice --- 


using System;
using System.IO;
using System.Net;
using System.Threading;
using Microsoft.WindowsMobile.Status;
using svs.Mobile.BeyondMedia.FeedCore.Helpers;

namespace svs.Mobile.BeyondMedia.FeedCore.RSS
{
	public class DownloadAgent
	{
		#region Delegates

		public delegate void DownloadCompletedEvent(string destination, bool ContentUpdated);

		public delegate void DownloadErrorEvent(string destination, Exception ex);

		public delegate void DownloadEvent(string destination);

		public delegate void DownloadProgressEvent(string destination, long current, long total);

		#endregion

		private readonly string m_Destination;
		private readonly Uri m_Source;
		private bool m_AllowResume = true;
		private Thread m_DownloadThread = null;
		private bool m_DownloadThreadIsDownloading;
		private DateTime? m_IgnoreIfNotModifiedSinceDate = null;
		private long? m_IgnoreIfSize = null;
		private Stream m_LocalStream = null;
		private long? m_ResourceContentLength = -1;
		private DateTime? m_ResourceLastModified = null;
		private DownloadStatistics m_Statistics;
		private bool m_StopDownload;
		private HttpWebRequest m_WebRequest = null;
		private HttpWebResponse m_WebResponse = null;
		private Stream m_WebResponseStream = null;
		private readonly NetworkCredential m_Credentials = null;

		#region Constructor

		public DownloadAgent(Uri source, string destination, NetworkCredential credentials)
		{
			if (source == null)
				throw new ArgumentNullException("Download agent was created with an empty URL!");

			if (string.IsNullOrEmpty(destination))
				throw new ArgumentNullException("Download agent was created with an empty download destination!");

			m_Destination = destination;
			m_Source = source;
			m_Credentials = credentials;
		}

		#endregion

		#region Public Methods

		public void StartDownload()
		{
			if (m_DownloadThread != null)
			{
				throw new ApplicationException("You must use a new instance of this class for each new download!");
			}

			if (m_DownloadThreadIsDownloading)
			{
				throw new ApplicationException("Downlod is already in progress!");
			}
			else
			{
				m_DownloadThreadIsDownloading = true;
				m_DownloadThread = new Thread(Download);
				m_DownloadThread.Name = "Download thread: " + m_Source;
				m_DownloadThread.IsBackground = true;
				m_DownloadThread.Start();
			}
		}

		public void Stop()
		{
			CoreHelper.WriteTraceEntry(">> Received Stop command to terminate the Download of: " + m_Source);

			if (!IsDownloading)
			{
				CoreHelper.WriteTraceEntry(">> Download is already stopped!");
				return;
			}

			CoreHelper.WriteTraceEntry(">> Download is in progress! Waitng for the download thread to stop!");

			m_StopDownload = true;

			if (m_WebRequest != null)
			{
				if (!m_WebRequest.HaveResponse)
				{
					CoreHelper.WriteTraceEntry(">> Awaitng Response! Canceling the request...");
					m_WebRequest.Abort();
				}
				else
				{
					CoreHelper.WriteTraceEntry(">> Got Response and downloading. Setting the abort flag...");
				}
			}

			if (DownloadTerminated != null)
				DownloadTerminated(m_Destination);

			CoreHelper.WriteTraceEntry(">> Stop Download operation complete!");
		}

		#endregion

		#region Public Properties

		/// <summary>
		/// The size of the content as reported by the server
		/// </summary>
		public long? ResourceContentLength
		{
			get { return m_ResourceContentLength; }
		}

		/// <summary>
		/// The last modified date 
		/// </summary>
		public DateTime? LastModifiedDate
		{
			get { return m_ResourceLastModified; }
		}

		/// <summary>
		/// True if we are currently downloading
		/// </summary>
		public bool IsDownloading
		{
			get { return m_DownloadThreadIsDownloading; }
		}

		/// <summary>
		/// If the destination file exists, but is not completely downloaded, allow resume will attempt to resume the download
		/// </summary>
		public bool AllowResume
		{
			get { return m_AllowResume; }
			set { m_AllowResume = value; }
		}

		public long? IgnoreIfSize
		{
			get { return m_IgnoreIfSize; }
			set { m_IgnoreIfSize = value; }
		}

		public DateTime? IgnoreIfNotModifiedSinceDate
		{
			get { return m_IgnoreIfNotModifiedSinceDate; }
			set { m_IgnoreIfNotModifiedSinceDate = value; }
		}


		public DownloadStatistics Statistics
		{
			get { return m_Statistics; }
		}

		#endregion

		#region Private Methods

		private void Download()
		{
			m_StopDownload = false;
			m_Statistics = new DownloadStatistics();

			try
			{
				long startPoint = 0;

				if (AllowResume && File.Exists(m_Destination))
				{
					startPoint = new FileInfo(m_Destination).Length;
				}

				Statistics.StartPosition = startPoint;
				Statistics.Url = m_Source.ToString();
				Statistics.SkipSize = IgnoreIfSize;
				Statistics.SkipDate = IgnoreIfNotModifiedSinceDate;

				m_ResourceLastModified = null;

				if (DownloadStarted != null)
					DownloadStarted(m_Destination);

				CoreHelper.WriteTraceEntry("");
				CoreHelper.WriteTraceEntry(">> -------------- Download of " + m_Source + " started! --------------");

				//Block until we get an internet connection
				WaitForInternetConnection();

				if(m_StopDownload)
				{
					CoreHelper.WriteTraceEntry(">> Wait for Connection Canceled!");
					Statistics.Terminated = true;
					DoCleanup(false);
					return;
				}

				int startPointInt = Convert.ToInt32(startPoint);

				m_WebRequest = (HttpWebRequest) WebRequest.Create(m_Source);
				m_WebRequest.UserAgent = Configuration.UserAgent;
				m_WebRequest.Accept = "*/*";

				if (StreamCompressPlugin.IsAvailable)
					m_WebRequest.Headers["Accept-Encoding"] = "gzip, deflate";

				//Check if the content has changed
				if (IgnoreIfNotModifiedSinceDate.HasValue)
				{
					m_WebRequest.IfModifiedSince = IgnoreIfNotModifiedSinceDate.Value;
				}

				m_WebRequest.Timeout = 30*1000;
				m_WebRequest.ReadWriteTimeout = 30*1000;

				if (startPointInt > 0)
					m_WebRequest.AddRange(startPointInt);

				//If the feed has authentication credentials, add them to the cache
				m_WebRequest.Credentials = m_Credentials != null ?
																new CredentialCache { { m_Source, "Basic", m_Credentials} } :
																CredentialCache.DefaultCredentials;	

				Statistics.EventDate = DateTime.Now;

				int tRequestStart = Environment.TickCount;
				CoreHelper.WriteTraceEntry(">> Sending Request...");

				CoreHelper.KeepDeviceAwake();
				m_WebResponse = (HttpWebResponse) m_WebRequest.GetResponse();
				CoreHelper.KeepDeviceAwake();

				int tGotResponse = Environment.TickCount;
				Statistics.DownloadTime = Statistics.TimeToFirstByte = (tGotResponse - tRequestStart);
				CoreHelper.WriteTraceEntry(">> Got Response...");

				m_ResourceContentLength = m_WebResponse.ContentLength;

				try
				{
					m_ResourceLastModified = m_WebResponse.LastModified;
				}
				catch
				{
					//Some servers (like facebook.com) put an invalid Last Modified date
					m_ResourceLastModified = null;
				}

				//Check if we have enough space to recive the download
				long requredLength = 2000*1024;
				if (m_WebResponse.ContentLength > 0)
					requredLength = m_WebResponse.ContentLength;

				if (!FlashStorageLocation.CheckIfSpaceIsAvailable(Path.GetDirectoryName(m_Destination), requredLength))
				{
					throw new CoreHelper.IssuficientStorageSpaceException(
						"Insufficient disk space to complete the download! Required: " + CoreHelper.GetFileLengthAsString(requredLength));
				}

				// Prepare to read the response
				Statistics.TotalSize = m_WebResponse.ContentLength;
				Statistics.ContentLastModifyDate = m_ResourceLastModified;
				Statistics.ContentEncoding = m_WebResponse.ContentEncoding;

				CoreHelper.WriteTraceEntry(">> Getting Response Stream...");
				m_WebResponseStream = GetResponseStream();

				m_LocalStream =
					new FileStream(m_Destination, startPointInt > 0 ? FileMode.Append : FileMode.Create, FileAccess.Write,
					               FileShare.Read);
				int bytesSize;
				byte[] downBuffer = new byte[50*1024];

				// Start reading the response
				while (!m_StopDownload && (bytesSize = m_WebResponseStream.Read(downBuffer, 0, downBuffer.Length)) > 0)
				{
					CoreHelper.WriteTraceEntry(">> Read " + bytesSize + " bytes");
					m_LocalStream.Write(downBuffer, 0, bytesSize);
					OnDownloadProgress(m_LocalStream.Length, (m_ResourceContentLength ?? 0) + startPointInt);
				}

				Statistics.DownloadedSize = m_LocalStream.Length;

				DoCleanup(false);

				if (!m_StopDownload)
				{
					CoreHelper.WriteTraceEntry(">> Download completed!");
					Statistics.CompletedNormaly = true;
					Statistics.DownloadTime = Environment.TickCount - tGotResponse;
					OnDownloadCompleted(true);
				}
				else
				{
					CoreHelper.WriteTraceEntry(">> Download Canceled!");
					Statistics.Terminated = true;
				}
			}
			catch (ThreadAbortException ex)
			{
				DoCleanup(true);
				CoreHelper.WriteTraceEntry(">> The download thread was aborted when downloading: " + m_Source + ". Reason: " +
				                           ex.Message);
			}
			catch (WebException ex)
			{
				//Not modified actually throws an exception
				if (ex.Status == WebExceptionStatus.ProtocolError && ((HttpWebResponse)ex.Response).StatusCode == HttpStatusCode.NotModified)
				{
					Statistics.DownloadSkipped = true;
					CoreHelper.WriteTraceEntry(">> Download skipped!");
					DoCleanup(false);
					OnDownloadProgress(m_ResourceContentLength ?? 0, m_ResourceContentLength ?? 0);
					OnDownloadCompleted(false);
					return;
				}

				DoCleanup(true);

				//Request canceled!
				if (ex.Status != WebExceptionStatus.RequestCanceled)
					OnDownloadError(ex);
			}
			catch (Exception ex)
			{
				CoreHelper.WriteLogEntry(">> Error downloading: " + m_Source + ". Reason: " + ex);
				DoCleanup(true);
				OnDownloadError(ex);
			}
			finally
			{
				CoreHelper.WriteTraceEntry(">> Download() completed. Thread is terminating... " + Thread.CurrentThread.Name);
			}
		}

		private void DoCleanup(bool abort)
		{
			CoreHelper.WriteTraceEntry(">> Cleanup Started... " + m_DownloadThread.Name);
			CoreHelper.KeepDeviceAwake();

			m_DownloadThreadIsDownloading = false; // we are in the process of cleanning up

			try
			{

				if (m_LocalStream != null)
				{
					CoreHelper.WriteTraceEntry(">>   Closing local stream..." + m_DownloadThread.Name);
					m_LocalStream.Close();
					CoreHelper.WriteTraceEntry(">>   Closed!" + m_DownloadThread.Name);
				}

				if (m_WebRequest != null)
				{
					if (m_WebResponseStream != null)
					{
						if (m_WebResponse != null)
						{
							CoreHelper.WriteTraceEntry(">>   Closing Response object... " + m_DownloadThread.Name);
							m_WebResponse.Close();
							CoreHelper.WriteTraceEntry(">>   Closed! " + m_DownloadThread.Name);
						}

						CoreHelper.WriteTraceEntry(">>   Closing response stream... " + m_DownloadThread.Name);
						m_WebResponseStream.Close();
						CoreHelper.WriteTraceEntry(">>   Closed!" + m_DownloadThread.Name);
					}

					if (abort)
					{
						CoreHelper.WriteTraceEntry(">>   Aborting the Web Request..." + m_DownloadThread.Name);
						m_WebRequest.Abort();
						CoreHelper.WriteTraceEntry(">>   Abortied!" + m_DownloadThread.Name);
					}
				}
			}
			catch (Exception ex)
			{
				CoreHelper.WriteTraceEntry(">> Unable to close the HTTP stream. Reason: " + ex.Message + "---" +
				                           m_DownloadThread.Name);
			}
			finally
			{
				m_WebRequest = null;
				m_WebResponseStream = null;
				m_WebResponse = null;
				m_LocalStream = null;
			}

			CoreHelper.WriteTraceEntry(">> Cleanup Complete..." + m_DownloadThread.Name);
		}

		private Stream GetResponseStream()
		{
			Stream retVal = null;

			if (StreamCompressPlugin.IsAvailable)
			{
				//select the right decompression stream
				if (m_WebResponse.ContentEncoding == "gzip")
				{
					retVal = StreamCompressPlugin.CreateGZipInputStream(m_WebResponse.GetResponseStream());
				}
				else if (m_WebResponse.ContentEncoding == "deflate")
				{
					retVal = StreamCompressPlugin.CreateInflaterInputStream(m_WebResponse.GetResponseStream());
				}
				else
				{
					retVal = m_WebResponse.GetResponseStream();
				}
			}

			if (retVal == null)
				retVal = m_WebResponse.GetResponseStream();

			return retVal;
		}

		private void OnDownloadError(Exception ex)
		{
			Statistics.LastException = ex;

			/*WebException we = ex as WebException;
			CoreHelper.WriteLogEntry(
				string.Format(">> Error downloading content from: {0}. Reason: {1}, Http Status: {2}", m_Source, ex.Message,
				              we != null ? we.Status.ToString() : "N/A"));*/

			if (DownloadError != null)
				DownloadError(m_Destination, ex);
		}

		private void OnDownloadProgress(long current, long total)
		{
			CoreHelper.KeepDeviceAwake();

			if (!m_StopDownload)
			{
				if (DownloadProgress != null)
					DownloadProgress(m_Destination, current, total);
			}
		}

		private void OnDownloadCompleted(bool hasnewContent)
		{
			m_DownloadThreadIsDownloading = false;

			if (DownloadCompleted != null)
				DownloadCompleted(m_Destination, hasnewContent);
		}

		private void WaitForInternetConnection()
		{
			const int maxRetry = 7;
			int retry = 0;

			while (!m_StopDownload && !HasNetworkConnection() && retry <= maxRetry)
			{
				CoreHelper.KeepDeviceAwake();
				CoreHelper.WriteLogEntry(">> Waiting for internet connection ... " + retry + " (Available Connections: " + SystemState.ConnectionsCount + ")");
				Thread.Sleep(5*1000); // wait for 5 secs
				retry++;
			}

			if (retry > maxRetry)
			{
				string Message = "Unable to establish internet connection!";

				if (Configuration.AllowCellularConnections)
					Message += " (GPRS/EDGE/3G connections are allowed)";
				else 
					Message += " (GPRS/EDGE/3G connections are NOT allowed. You can allow the use of GPRS/EDGE/3G connections in Tools > Settings > General Settings)";

				throw new CoreHelper.UnableToEstablishNetworkConnectionException(Message);
			}
		}

		private static bool HasNetworkConnection()
		{
			if(Configuration.AllowCellularConnections)
				return true;

			return SystemState.ConnectionsCount - SystemState.ConnectionsCellularCount > 0;
		}

		#endregion

		public event DownloadEvent DownloadStarted;

		public event DownloadCompletedEvent DownloadCompleted;

		public event DownloadEvent DownloadTerminated;

		public event DownloadProgressEvent DownloadProgress;

		public event DownloadErrorEvent DownloadError;
	}
}